﻿#region License
/*
Copyright (c) 2010, G.W. van der Vegt
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided 
that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the 
  following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 
  the following disclaimer in the documentation and/or other materials provided with the distribution.

* Neither the name of G.W. van der Vegt nor the names of its contributors may be 
  used to endorse or promote products derived from this software without specific prior written 
  permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 
THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
*/
#endregion License

#region changelog

//----------   ---   -------------------------------------------------------------------------------
//Purpose:           Simple Profiling with minimal impact on performance.
//Usage:             

/*
                     Example Usage :
                         
                     using (Profiling pf as new Profiling("name") { 
                       Mark()
                         ...
                       Add() //Time Between Mark & Add is Added to the Total and Hits is Incremented by one.
                     }
 
                     or
  
                     Profiling.Hit("Name") -> Increments Hit by one.

                     using (new Profiling("aname") { 
                         ...
                     }

                     Generated an namually named Profiling Object that generates output to the Debug Window when Disposed.
                     Add, Hit and Mark are automatically called Attribute creation and dispose.

                     using (new Profiling(this) { 
                         ...
                     }

                     Generated an automatically named Profiling Object that generates output to the Debug Window when Disposed.
                     The name is based on the callers class and methodname.
                     Add, Hit and Mark are automatically called Attribute creation and dispose.
  
  
                     using (Profiling p = new Profiling())
                     {
                        foreach (String name in p)
                        {
                            Console.WriteLine(p[name]);
                            //or
                            //Console.WriteLine(Profiling.ToString(name));
                        }
                     }
  
                     Dumps all ProfileData Objects to the Console Window.

*/

//By:                G.W. van der Vegt (wvd_vegt@knoware.nl)
//Url:               http://swiss.codeplex.com
//Depends:           -
//Based on:          Heavily used Borland Delphi code of the Author.
//License:           New BSD License
//----------   ---   -------------------------------------------------------------------------------
//dd-mm-yyyy - who - description
//----------   ---   -------------------------------------------------------------------------------
//01-02-2010 - veg - Creating (Porting Delphi Code).
//                 - Added Mark/Add/Hit routines.
//                 - Added non static version that automaticallt calls Marks and Add in constructor/dispose.
//                 - Result is that 'using (new Profiling("name")) { ... }' works properly and correctly
//                   reports the execution time and has a Hits value of 1.
//                 - Added test suite.
//                 - Added IEnumerable Interface for Keys of DataList.
//                 - The static methods take the Name of the Profile as a paramater, the non static methods
//                   use the Name parameter passed into the Constructor as Profile Name.
//                 - Added to swiss.codeplex.com.
//04-02-2010 - veg - Added new constructor that takes the calling object and automatically generates a name.
//                   NOTE: overloaded methods are threated as one for now.
//                 - Added automatic output to the console when the Profiling object is disposed.
//                 - Changed namespace to Swiss.
//                 - Added Indenting to output (switched from Console.WriteLine() to Debug.WriteLine()).
//                 - Changed GetMethodDescription so it can cope with overloaded methods.
//23-05-2010 - veg - Added NoOfHits to Hits and Add so you can adjust hits better.
//                 - Added Average to output.
//                 - Added Verbose flag to suppres automatic IDisposable output.
//                 - Added explicit usage of static routines by adding Profiling. prefix to certain calls.
//                 - Unified ToString() to a single static version.
//----------   ---   -------------------------------------------------------------------------------
//dd-mm-yyyy         todo's
//----------   ---   -------------------------------------------------------------------------------
//                 - Allow Static GetEnumerator().
//                 - Look into format returns by Elapsed.
//                 - A Single Instance of DataList across the application.
//                 - Solve the problem of Lists and Structs (turn ProfileData into a struct).
//                 - Does not work properly when recursing without additional add/mark().
//----------   ---   -------------------------------------------------------------------------------

#endregion changelog

namespace Swiss
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Linq;
    using System.Reflection;

    /// <summary>
    /// Implements a simple to use Profiling class for Instrumented Profiling.
    /// </summary>
    public class Profiling : IDisposable, IEnumerable<String>
    {
        private static Dictionary<String, ProfileData> DataList = new Dictionary<String, ProfileData>();

        /// <summary>
        /// Supresses Output when using IDisposable method.
        /// </summary>
        public static Boolean Verbose = false;

        #region Instance

        /// <summary>
        /// Name of the ProfileData instance, used to identify it.
        /// </summary>
        public String Name = null;

        /// <summary>
        /// Name of the Module calling.
        /// </summary>
        public String Module = null;

        /// <summary>
        /// Creates a anonymous Profiling Object that can only be used to 
        /// to enumerate all profiles or for timing if the name is set.
        /// 
        /// Note: This constructor does not generate a ProfileData Object Instance!
        /// </summary>
        public Profiling()
        {
            Name = null;
            Module = null;
        }

        /// <summary>
        /// Creates an automatically named ProfileData 
        /// using either the DescriptionAttribute value or (Method)Name of the caller.
        /// </summary>
        /// <param name="o">Pass this of the calling Class</param>
        public Profiling(Object o)
            : this()
        {
            this.Name = GetMethodDescription(o);
            this.Module = GetModuleDescription(o);

            if (Verbose)
            {
                Debug.WriteLine(String.Format("Auto Mark for {0}", Name));
                if (!String.IsNullOrEmpty(Module))
                {
                    Debug.WriteLine(String.Format("Originating from {0}", Module));
                }
                Debug.Indent();
            }

            Profiling.GetData(Name).Mark();
        }

        /// <summary>
        /// Creates a named ProfileData.
        /// </summary>
        /// <param name="Name">The Name used to identify the ProfileData</param>
        public Profiling(String Name)
            : this()
        {
            this.Name = Name;

            if (Verbose)
            {
                Debug.WriteLine(String.Format("Auto Mark for {0}", Name));
                Debug.Indent();
            }

            Profiling.GetData(Name).Mark();
        }

        /// <summary>
        /// Allows access to ProfileData results by Name.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        /// <returns>a String containing the ProfileData</returns>
        public String this[String Name]
        {
            get { return Profiling.ToString(Name); }
        }

        /// <summary>
        /// Allows access to ProfileData results by Index.
        /// </summary>
        /// <param name="Index">The index of the ProfileData</param>
        /// <returns>a String containing the ProfileData</returns>    
        public String this[Int32 Index]
        {
            get { return Profiling.ToString(DataList.Keys.ElementAt<String>(Index)); }
        }

        /// <summary>
        /// Returns the Number of ProfileData Objects.
        /// </summary>
        public Int32 Count
        {
            get { return Profiling.DataList.Keys.Count; }
        }

        /// <summary>
        /// Marks the current Time.
        /// </summary>
        public void Mark()
        {
            if (!String.IsNullOrEmpty(Name))
            {
                Mark(Name);
            }
        }

        /// <summary>
        /// Adds the Time between the Mark() and Add() calls to the Elapsed time.
        /// </summary>
        public void Add()
        {
            if (!String.IsNullOrEmpty(Name))
            {
                Add(Name);
            }
        }

        /// <summary>
        /// Adds the Time between the Mark() and Add() calls to the Elapsed time.
        /// </summary>
        public void Add(Int32 NoOfHits)
        {
            if (!String.IsNullOrEmpty(Name))
            {
                Add(Name, NoOfHits);
            }
        }

        /// <summary>
        /// Increments the Hits of a ProfileData by 1.
        /// 
        /// Note: Each call to Add() and Dispose() will also cause a Hit to be added.
        /// </summary>
        public void Hit()
        {
            if (!String.IsNullOrEmpty(Name))
            {
                Hit(Name);
            }
        }

        /// <summary>
        /// Increments the Hits of a ProfileData by 1.
        /// 
        /// Note: Each call to Add() and Dispose() will also cause a Hit to be added.
        /// </summary>
        public void Hit(Int32 nohits)
        {
            if (!String.IsNullOrEmpty(Name))
            {
                Hit(Name, nohits);
            }
        }

        /// <summary>
        /// Resets a ProfileData Object.
        /// </summary>
        public void Clear()
        {
            if (!String.IsNullOrEmpty(Name))
            {
                Clear(Name);
            }
        }

        /// <summary>
        /// Returns the Elapsed Time in hh:mm:ss.mmm Format.
        /// </summary>
        public String Elapsed()
        {
            if (!String.IsNullOrEmpty(Name))
            {
                return Elapsed(Name);
            }
            else
            {
                return "na";
            }
        }

        /// <summary>
        /// Returns the Number of Hits counted.
        /// </summary>
        /// <returns>The Number of Hits</returns>
        public Int32 Hits()
        {
            if (!String.IsNullOrEmpty(Name))
            {
                return Hits(Name);
            }
            else
            {
                return 0;
            }
        }

        /// <summary>
        /// Disposes this Object.
        /// 
        /// Note: Dependend on the Name and if a Mark() call was still active, 
        ///       the last marked time is added.
        ///       Output to the Debug Window is generated if Verbose is true.
        /// </summary>
        public void Dispose()
        {
            if (!String.IsNullOrEmpty(Name))
            {
                Add(Name);

                if (Verbose)
                {
                    Debug.Unindent();

                    Debug.WriteLine(String.Format("Auto Add/Hit for {0}", Name));
                    Debug.WriteLine(ToString());
                }
            }
        }

        /// <summary>
        /// Generates a String containing the ProfileData.
        /// </summary>
        /// <returns>a String containing the ProfileData</returns>
        public override String ToString()
        {
            return Profiling.ToString(Name);
        }

        #endregion Instance

        #region Static

        /// <summary>
        /// 
        /// </summary>
        /// <param name="Name"></param>
        /// <returns></returns>
        private static ProfileData GetData(String Name)
        {
            if (!Profiling.DataList.ContainsKey(Name))
            {
                if (Verbose)
                {
                    Debug.WriteLine("Adding profile: {0}", Name);
                }
                Profiling.DataList.Add(Name, new ProfileData());
            }
            return Profiling.DataList[Name];
        }

        /// <summary>
        /// Marks the current Time.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        public static void Mark(String Name)
        {
            GetData(Name).Mark();
        }

        /// <summary>
        /// Adds the Time between the Mark() and Add() calls to the Elapsed time.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        public static void Add(String Name)
        {
            GetData(Name).Add();
        }

        /// <summary>
        /// Adds the Time between the Mark() and Add() calls to the Elapsed time.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        /// <param name="NoOfHits">The number of hits to add</param>
        public static void Add(String Name, Int32 NoOfHits)
        {
            GetData(Name).Add(NoOfHits);
        }

        /// <summary>
        /// Increments the Hits of a ProfileData by 1.
        /// 
        /// Note: Each call to Add() and Dispose() will also cause a Hit to be added.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        public static void Hit(String Name)
        {
            GetData(Name).Hit();
        }

        /// <summary>
        /// Increments the Hits of a ProfileData by 1.
        /// 
        /// Note: Each call to Add() and Dispose() will also cause a Hit to be added.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        /// <param name="NoOfHits">The number of hits to add</param>
        public static void Hit(String Name, Int32 NoOfHits)
        {
            GetData(Name).Hit(NoOfHits);
        }

        /// <summary>
        /// Resets a ProfileData Object.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        public static void Clear(String Name)
        {
            GetData(Name).Clear();
        }

        /// <summary>
        /// Removes a ProfileData Object from the Collection.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        /// <returns>True if the Name identified an existing ProfileData Object</returns>
        public static Boolean Remove(String Name)
        {
            return DataList.Remove(Name);
        }

        /// <summary>
        /// Returns the Elapsed Time in hh:mm:ss.mmm Format.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        /// <returns>The Elapsed Time</returns>
        public static String Elapsed(String Name)
        {
            return GetData(Name).elapsed.ToString();
        }

        /// <summary>
        /// Returns the Elapsed Time in hh:mm:ss.mmm Format.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        /// <returns>The Elapsed Time</returns>
        public static String Average(String Name)
        {
            Int32 hits = GetData(Name).hits;

            if (hits != 0)
            {
                Double ticks = GetData(Name).elapsed.TotalMilliseconds;

                return (ticks / hits).ToString();
            }
            else
            {
                return "na";
            }
        }

        /// <summary>
        /// Returns the Number of Hits counted.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        /// <returns>The Number of Hits</returns>
        public static Int32 Hits(String Name)
        {
            return GetData(Name).hits;
        }

        /// <summary>
        /// Generates a String containing the ProfileData.
        /// </summary>
        /// <param name="Name">The name of the ProfileData</param>
        /// <returns>a String containing the ProfileData</returns>
        public static String ToString(String Name)
        {
            if (DataList.ContainsKey(Name))
            {
                if (Hits(Name) > 1)
                {
                    return String.Format("{0} Hit(s) for {1} in {2} [sec] (avg {3} [msec/hit]).", Hits(Name), Name, Elapsed(Name), Average(Name));
                }
                else
                {
                    return String.Format("{0} Hit(s) for {1} in {2} [sec].", Hits(Name), Name, Elapsed(Name));
                }
            }
            else
            {
                return String.Format("ProfileData not found");
            }
        }

        /// <summary>
        /// Dumps all Profiling Results to the Console.
        /// </summary>
        public static void ShowResults()
        {
            Console.WriteLine("-----");
            foreach (KeyValuePair<String, ProfileData> kvp in DataList)
            {
                Console.WriteLine(ToString(kvp.Key));
            }
            Console.WriteLine("-----");
        }

        /// <summary>
        /// Write the current time to the Console
        /// </summary>
        public static void ConsoleStamp()
        {
            ConsoleStamp(null);
        }

        /// <summary>
        /// Write the current time and an optional Message to the Console
        /// </summary>
        /// <param name="Message">The optional Message</param>
        public static void ConsoleStamp(String Message)
        {
            if (!String.IsNullOrEmpty(Message))
            {
                Console.WriteLine("{0} - {1}", DateTime.Now.ToLongTimeString(), Message);
            }
            else
            {
                Console.WriteLine("{0}", DateTime.Now.ToLongTimeString());
            }
        }

        #endregion Static

        /// <summary>
        /// Either try to return the value of a DescriptionAttribute of the caller or it's class and name.
        /// 
        /// Note: This function is called from a constructor and skips the stack enough to get to the 
        /// actual caller outside this class.
        /// </summary>
        /// <param name="o">The calling method's object</param>
        /// <returns>The value of a DescriptionAttribute of the caller or it's class and name</returns>
        private String GetMethodDescription(Object o)
        {
            StackFrame fr = new StackFrame(2);
            MethodInfo mi = (MethodInfo)fr.GetMethod();

            if (mi != null)
            {
                Object[] atts = mi.GetCustomAttributes(typeof(DescriptionAttribute), false);
                if (atts != null && atts.Length != 0)
                {
                    DescriptionAttribute da = (DescriptionAttribute)atts[0];
                    if (da != null)
                    {
                        return String.Format("\"{0}\"", da.Description);
                    }
                }
            }

            return String.Format("{0}.{1}()", o.GetType().Name, mi.Name);
        }

        /// <summary>
        /// Either try to return the name of the Module of the caller.
        /// 
        /// Note: This function is called from a constructor and skips the stack enough to get to the 
        /// actual caller outside this class.
        /// </summary>
        /// <param name="o">The calling method's object</param>
        /// <returns>The name of the Module of the caller</returns>
        private String GetModuleDescription(Object o)
        {
            StackFrame fr = new StackFrame(2);
            MethodInfo mi = (MethodInfo)fr.GetMethod();
            if (mi != null)
            {
                return mi.Module.Assembly.FullName;
            }
            else
            {
                return "na";
            }
        }

        #region IDisposable Members

        void IDisposable.Dispose()
        {
            this.Dispose();
        }

        #endregion

        #region IEnumerable<string> Members

        /// <summary>
        /// Returns all Names of Profiling Entries
        /// </summary>
        /// <returns></returns>
        public IEnumerator<string> GetEnumerator()
        {
            return Profiling.DataList.Keys.GetEnumerator();
        }

        #endregion IEnumerable<string>

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return Profiling.DataList.Keys.GetEnumerator();
        }

        #endregion IEnumerable Members

        #region Other

        /// <summary>
        /// Contains the actual Profiling Data on a single Profiling Entry.
        /// </summary>
        public class ProfileData
        {
            internal Stopwatch stopwatch = Stopwatch.StartNew();

            internal Int32 hits
            {
                get;
                set;
            }

            internal TimeSpan elapsed = new TimeSpan();

            internal ProfileData()
            {
                Clear();
            }

            internal void Mark()
            {
                stopwatch.Reset();
                stopwatch.Start();
            }

            internal void Add()
            {
                Add(1);
            }

            internal void Add(Int32 NoOfHits)
            {
                if (stopwatch.IsRunning)
                {
                    stopwatch.Stop();
                    elapsed += stopwatch.Elapsed;

                    hits += NoOfHits;
                }
            }

            internal void Hit()
            {
                hits++;
            }

            internal void Hit(Int32 nohits)
            {
                hits += nohits;
            }

            internal void Clear()
            {
                stopwatch.Reset();

                hits = 0;

                elapsed = default(TimeSpan);
            }
        }

        #endregion Other

    }
}
